테스트를 위한 도구
단위 테스트를 작성하고 실행하기 위한 두 가지 프레임 워크를 설명한다.
- unittest
- pytest
Merge Request
코드 리뷰를 도와주는 간단한 버전 제어 도구로 몇 가지 전제를 가진다.
- 한 명 이상의 사용자가 변경 내용에 동의하지 않으면 거절된다.
- 아무도 반대하지 않은 상태에서 두 명 이상의 개발자가 동의하면 승인된다.
- 이외의 상태는 보류한다.
from enum import enum class MergeRequestStatus(Enum): APPROVED = "approved" REJECTED = "rejected" PENDING = "pending" OPEN = "open" CLOSED = "closed" class MergeRequest: def __init__(self): self._context = { "upvotes": set(), "downvotes": set(), } self._status = MergeRequestStatus.OPEN @property def status(self): if self._context["downvotes"] return MergeRequestStatus.REJECTED elif len(self._context["upvotes"]) >= 2: return MergeRequestStatus.APPROVED return MergeRequestStatus.PENDING def upvote(self, by_user): self._context["downvotes"].discard(by_user) self._context["upvotes"].add(by_user) def downvote(self, by_user): self._context["upvotes"].discard(by_user) self._context["downvotes"].add(by_user) def close(self): self._status = MergeRequestStatus.CLOSED def _cannot_vote_if_closed(self): if self._stutus == MergeRequestStatus.CLOSED: raise MergeRequestException("종료된 Merge Request에 투표할 수 없습니다")
이 코드를 두 가지 단위 테스트 도구(unittest, pytest)로 살펴보자.
unittest
- 거의 모든 종류의 테스트를 작성할 수 있는 풍부한 API를 제공
- 표준 라이브러리에 포함되어 편리한 사용
- 자바의 Junit 기반
- 객체를 사용하여 작성되며, 클래스의 시나로오별로 테스트를 그룹화
class TestMergeRequestStatus(unittest.TestCase): def test_simple_rejected(self): merge_request = MergeRequest() merge_request.downvote("maintainer") self.assertEqual(merge_request.status, MergeRequestStatus.REJECTED) def test_just_created_is_pending(self): self.assertEqual(MergeRequest().status, MergeRequestStatus.PENDING) def test_pending_awaiting_review(self): merge_request = MergeRequest() merge_request.upvote("core-dev") self.assertEqual(merge_request.status, MergeRequestStatus.PENDING) def test_approved(self): merge_request = MergeRequest() merge_request.upvotes("dev1") merge_request.upvotes("dev2") self.assertEqual(merge_request.status, MergeRequestStatus.APPROVED) def test_cannot_upvote_on_closed_merge_request(self): self.merge_request.close() self.assertRaises( MergeRequestException, self.merge_request.upvote, "dev1", ) def test_cannot_downvote_on_closed_merge_request(self): self.merge_request.close() self.assertRaises( MergeRquestException, "종료된 Merge Request에 투표할 수 없음", self.merge_request.downvote, "dev1", )
예외가 발생하는지 뿐 아니라 오류 메시지도 확인하자.
발생한 예외가 정확히 우리가 원했던 예외인지 확인하기 위함이다. 우연히 같은 타입의 예외가 발생했으나 실제로는 다른 원인에 의한 경우를 제외하기 위한 것이다.
제공 API
이름 | 내용 |
assertEqual | 실제 값과 예상 값을 비교 |
assertRaises | 특정 예외가 발생했는지 확인 |
테스트 파라미터화
중복된 테스트 코드를 만드는 대신 하나의 테스트에 적절한 옵션을 넘겨서 구분한다.
상태와 관련된 코드를 클래스로 분리하고 새롭게 추상화한다.
class AcceptanceThreshold: def __init__(self, merge_request_context: dict) -> None: self._context = merge_request_context def status(self): if self._context["downvotes"]: return MergeRequestStatus.REJECTED elif len(self._context["upvotes"]) >= 2: return MergeRequestStatus.APPROVED return MergeRequestStatus.PENDING class MergeRequest: ... @property def status(self): if self._status == MergeReqeustStatus.CLOSED: return self._status return AcceptanceThreshold(self._context).status()
class TestAccrptanceThreshold(unittest.TestCase): def setUp(self): self.fixture_data = ( ( {"downvotes": set(), "upvotes": set()}, MergeRequestStatus.PENDING ), ( {"downvotes": set(), "upvotes": {"dev1"})}, MergeRequestStatus.PENDING ), ( {"downvotes": "dev1", "upvotes": set()}, MergeRequestStatus.REJECTED ), ( {"downvotes": set(), "upvotes": {"dev1", "dev2"}}, MergeRequestStatus.APPROVED ), ) def test_status_resolution(self): for context, expected in self.fixture_data with self.subTest(context=context) status = AcceptanceThreshold(context).status() self.assertEqual(status, expected)
제공 API
이름 | 내용 |
setUp | - 테스트 케이스 클래스 내의 각 테스트 메서드를 실행하기 전에 호출
- 테스트 실행 전에 필요한 초기 설정 작업을 수행하는 데 사용 |
subTest | - 테스트 케이스 내에서 여러 다른 조건이나 매개변수에 대해 하위 테스트(sub-tests)를 실행할 수 있게 해주는 기능
- 각각의 시나리오는 독립적으로 평가 |
테스트에 파라미터를 사용하는 경우 각 인스턴스에 최대한 많은 컨텍스트 정보를 제공하여 오류 발생 시 디버깅을 쉽게 한다.
pytest
- 더 간결하고 유연한 문법을 제공
- 함수 기반 테스트를 작성 가능
fixture
기능을 통한 테스트 데이터와 환경 설정
- 실패한 테스트에 대해 자세한 정보와 함께 풍부한 에러 리포팅을 제공
unittest
로 작성된 코드도 실행할 수 있기 때문에 점진적인 교체도 가능
pytest 기초적 사용
def test_simple_rejected(): merge_request = MergeRequest() merge_request.downvote("maintainer") assert merge_request.status == MergeRequestStatus.REJECTED def test_invaild_types(): merge_request = MergeRequest() pytest.raises(TypeError, merge_request.upvote, {"invalid-object"}) def test_cannot_vote_on_closed_merge_request(): merge_request = MergeRequest() merge_request.close() pytest.raises(MergeRquestException, merge_request.upvote, "dev1") with pytest.raises( MergeRequestException, match="종료된 Merge Request에 투표할 수 없음", ): merge_request.downvote("dev1")
@pytest.mark.parametrize("context, expected_status",( ( {"downvotes": set(), "upvotes": set()}, MergeRequestStatus.PENDING, ), ( {"downvotes": set(), "upvotes": {"dev1"}}, MergeRequestStatus.PENDING, ), ( {"downvotes": {"dev1"}, "upvotes": set()}, MergeRequestStatus.REJECTED ), ( {"downvotes": set(), "upvotes": {"dev1", "dev2"}}, MergeRequestStatus.APPROVED ), ),) def test_acceptance_threshold_status_resolution(context, expected_status): assert AcceptanceThreshold(context).status() == expected_status
unittest
에서 쓰여진 테스트 파라미터화와 비교해, 내부 for 루프와 중첩된 컨텍스트 관리자가 제거됐다(코드가 간결해졌다). 각 테스트의 데이터가 올바르게 분리됐고, 확장과 유지보수에 유리한 구조를 만들었다.@pytest.mark.parameterize
를 사용하여 반복을 없애고, 테스트 본문을 응집력 있게 유지한다. 테스트에 전달한 입력 값과 시나리오는 명시적으로 파라미터를 만들어 제공한다.
댓글